/*
 * @(#) $Header$
 *
 * Copyright (C) 2010  Daniel Léonard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package ca.forklabs.baselib.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.ChoiceFormat;
import java.text.MessageFormat;
import java.util.ResourceBundle;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.WindowConstants;
import ca.forklabs.baselib.awt.datatransfer.ClipboardUtil;
import ca.forklabs.baselib.util.CharsetPropertiesControl;

/**
 * Class {@code ExceptionDialog} shows an exception with its stacktrace.
 *
 * @author   <a href="mailto:forklabs at gmail.com?subject=ca.forklabs.baselib.swing.ExceptionDialog">Daniel Léonard</a>
 * @version $Revision$
 */
public class ExceptionDialog extends JDialog {

//---------------------------
// Class variables
//---------------------------

   /** serialVersionUID */
   private static final long serialVersionUID = -4483046436612504386L;

   /** The default depth the stack trace display goes. */
   public static final int DEFAULT_DEPTH = 5;


//---------------------------
// Constructors
//---------------------------

   /**
    * Constructor.
    * @param   throwable   the throwable to show the stack trace.
    */
   public ExceptionDialog(Throwable throwable) {
      this(throwable, true);
      }

   /**
    * Constructor.
    * @param   throwable   the throwable to show the stack trace.
    * @param   modal   the modality of the dialog.
    */
   public ExceptionDialog(Throwable throwable, boolean modal) {
      this(throwable, ExceptionDialog.DEFAULT_DEPTH, modal);
      }

   /**
    * Constructor.
    * @param   throwable   the throwable to show the stack trace.
    * @param   trace_depth   the size of visible stack trace.
    * @param   modal   the modality of the dialog.
    */
   public ExceptionDialog(Throwable throwable, int trace_depth, boolean modal) {
      this.setup(throwable, trace_depth, modal);
      }


//---------------------------
// Instance methods
//---------------------------

   /**
    * Gets the title of the dialog.
    * @return   the title.
    */
   protected String getTitleText() {
      String title = ExceptionDialog.getLocalizedString(ExceptionDialog.DIALOG_TITLE);
      return title;
      }

   /**
    * Sets up the title.
    */
   protected void setupTitle() {
      String title = this.getTitleText();
      this.setTitle(title);
      }

   /**
    * Makes a pretty-print of the {@code Throwable}'s stack trace.
    * @param   throwable   the exception.
    * @return   the stack trace.
    */
   @SuppressWarnings("nls")
   protected String toString(Throwable throwable) {
      String endline = System.getProperty("line.separator");

      StringBuilder sb = new StringBuilder();
      sb.append(throwable.getClass().getName());
      sb.append(": ");
      sb.append(throwable.getLocalizedMessage());
      sb.append(endline);

      StackTraceElement[] ste = throwable.getStackTrace();
      int len = ste.length;
      for (int i = 0; i < len; i++) {
         sb.append("   ");
         sb.append(ExceptionDialog.getLocalizedString(ExceptionDialog.AT));
         sb.append(" ");
         sb.append(ste[i]);
         sb.append(endline);
         }

      Throwable cause = throwable.getCause();
      if (null != cause) {
         String cause_trace = this.toString(cause);
         sb.append(ExceptionDialog.getLocalizedString(ExceptionDialog.CAUSED_BY));
         sb.append(cause_trace);
         }

      String string = sb.toString();
      return string;
      }

   /**
    * Sets up the copy to clipboard button.
    * @param   throwable   the throwable to show the stack trace.
    * @return   the copy button.
    */
   protected JButton setupCopyButton(Throwable throwable) {
      final Throwable exception = throwable;
      String text = ExceptionDialog.getLocalizedString(ExceptionDialog.COPY_TO_CLIPBOARD);
      JButton copy = new JButton(text);
      copy.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent ae) {
            String stack_trace = ExceptionDialog.this.toString(exception);
            ClipboardUtil.putInClipboard(stack_trace);
            }
         });

      return copy;
      }

   /**
    * Sets up the OK button.
    * @return   the OK button.
    */
   protected JButton setupOKButton() {
      final Window window = this;

      String text = ExceptionDialog.getLocalizedString(ExceptionDialog.OK);
      JButton ok = new JButton(text);
      ok.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent ae) {
            window.dispose();
            }
         });

      return ok;
      }

   /**
    * Sets up the option pane.
    * @param   throwable   the throwable to show the stack trace.
    * @param   trace_depth   the size of visible stack trace.
    * @return   the option pane.
    */
   @SuppressWarnings({
         "nls", "boxing"
   })
   protected JOptionPane setupOptionPane(Throwable throwable, int trace_depth) {
      StringBuilder sb = new StringBuilder();

      sb.append("<html>");

      sb.append(throwable.getClass().getName());
      sb.append(": ");
      sb.append(throwable.getLocalizedMessage());
      sb.append("<br>");

// BUG: using (trace_depth + 1), it seems to solve issue 15, still undecided in what to do with that
      StackTraceElement[] ste = throwable.getStackTrace();
      int len = ste.length;
      for (int i = 0; (i < trace_depth) && (i < len); i++) {
         sb.append("   ");
         sb.append(ExceptionDialog.getLocalizedString(ExceptionDialog.AT));
         sb.append(" ");
         sb.append(ste[i]);
         sb.append("<br>");
         }

      if (len > trace_depth) {
         double[] limits = { 1.0, 2.0, };
         String[] values = new String[] {
            ExceptionDialog.getLocalizedString(ExceptionDialog.MORE_SINGULAR),
            ExceptionDialog.getLocalizedString(ExceptionDialog.MORE_PLURAL),
            };
         ChoiceFormat choice_format = new ChoiceFormat(limits, values);

         int left = len - trace_depth;
         Object[] args = new Object[] {
            left,
            choice_format.format(left),
            };

         String template = ExceptionDialog.getLocalizedString(ExceptionDialog.MORE_TEMPLATE);
// NOTE : might need to apply the locale
         MessageFormat message_format = new MessageFormat(template);
         String message = message_format.format(args);
         sb.append(message);
         }

      sb.append("</html>");

      String raw = sb.toString();
      String cooked = raw.replaceAll(" ", "&nbsp;");

      JLabel label = new JLabel(cooked);
      int message_type = JOptionPane.ERROR_MESSAGE;
      int object_type = JOptionPane.DEFAULT_OPTION;
      JButton copy = this.setupCopyButton(throwable);
      JButton ok = this.setupOKButton();

      copy.setMargin(new Insets(2, 8, 2, 8));
      ok.setMargin(new Insets(2, 8, 2, 8));

      Object[] options = new Object[] { copy, ok };
      Object initial_option = ok;
      JOptionPane pane = new JOptionPane(label, message_type, object_type, null, options, initial_option);
      //JOptionPane pane = new JOptionPane(label, message_type, object_type, null, null, null);

      return pane;
      }

   /**
    * Sets up the body of the dialog.
    * @param   throwable   the throwable to show the stack trace.
    * @param   trace_depth   the size of visible stack trace.
    */
   protected void setupBody(Throwable throwable, int trace_depth) {
      LayoutManager layout = new BorderLayout();

      Component pane = this.setupOptionPane(throwable, trace_depth);

      Container content_pane = this.getContentPane();
      content_pane.removeAll();
      content_pane.setLayout(layout);
      content_pane.add(BorderLayout.CENTER, pane);
      }

   /**
    * Sets up the window behavior, dispose on close.
    * @param   modal   the modality of the dialog.
    */
   protected void setupWindowBehaviour(boolean modal) {
      this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
      this.setModal(modal);
      }

   /**
    * Sets up the dialog.
    * @param   throwable   the throwable to show the stack trace.
    * @param   trace_depth   the size of visible stack trace.
    * @param   modal   the modality of the dialog.
    */
   protected void setup(Throwable throwable, int trace_depth, boolean modal) {
      this.setupTitle();
      this.setupBody(throwable, trace_depth);
      this.setupWindowBehaviour(modal);
      this.pack();
      }


//---------------------------
// Class methods
//---------------------------

   /**
    * Shows a modal exception dialog.
    * @param   throwable   the exception to display.
    */
   public static void showExceptionDialog(Throwable throwable) {
      int depth = ExceptionDialog.DEFAULT_DEPTH;
      ExceptionDialog.showExceptionDialog(throwable, depth);
      }

   /**
    * Shows a modal exception dialog.
    * @param   throwable   the exception to display.
    * @param   depth   the number of stack traces to show.
    */
   public static void showExceptionDialog(Throwable throwable, int depth) {
      Component owner = null;
      ExceptionDialog.showExceptionDialog(owner, throwable, depth);
      }

   /**
    * Shows a model exception dialog.
    * @param   parent   the component to center on.
    * @param   throwable   the exception to display.
    */
   public static void showExceptionDialog(Component parent, Throwable throwable) {
      int depth = ExceptionDialog.DEFAULT_DEPTH;
      ExceptionDialog.showExceptionDialog(parent, throwable, depth);
      }

   /**
    * Shows a model exception dialog.
    * @param   parent   the component to center on.
    * @param   throwable   the exception to display.
    * @param   depth   the number of stack traces to show.
    */
   public static void showExceptionDialog(Component parent, Throwable throwable, int depth) {
      ExceptionDialog dialog = new ExceptionDialog(throwable, depth, true);
      dialog.setLocationRelativeTo(parent);
      dialog.setVisible(true);
      }


//---------------------------
// Localization
//---------------------------

   /** Key for the title of the dialog. */
   @SuppressWarnings("nls")
   public static final String DIALOG_TITLE = "dialog.title";
   /** Key for the at token in exception traces. */
   @SuppressWarnings("nls")
   public static final String AT = "at";
   /** Key for the label of the copy to clipboard button. */
   @SuppressWarnings("nls")
   public static final String COPY_TO_CLIPBOARD = "copy.to.clipboard";
   /** Key for the label of the ok button. */
   @SuppressWarnings("nls")
   public static final String OK = "ok";
   /** Key for the singular value of the more stacktraces when there are too many. */
   @SuppressWarnings("nls")
   public static final String MORE_SINGULAR = "more.singular";
   /** Key for the plural value of the more stacktraces when there are too many. */
   @SuppressWarnings("nls")
   public static final String MORE_PLURAL = "more.plural";
   /** Key for the template message when there are too many stacktraces. */
   @SuppressWarnings("nls")
   public static final String MORE_TEMPLATE = "more.template";
   /** Key for the caused by token. */
   @SuppressWarnings("nls")
   public static final String CAUSED_BY = "caused.by";

   /**
    * Gets the resource bundle itself.
    * @return   the resource bundle.
    */
   protected static ResourceBundle getResourceBundle() {
      String name = ExceptionDialog.class.getName();
      ResourceBundle bundle = ResourceBundle.getBundle(name, CharsetPropertiesControl.UTF_8_CONTROL);
      return bundle;
      }

   /**
    * Gets and formats the specified localized string from the menu resource
    * bundle.
    * @param   key   the key.
    * @param   arguments   the arguments to format the string.
    * @return   the value.
    */
   protected static String getLocalizedString(String key, Object... arguments) {
      String name = ExceptionDialog.class.getName();
      ResourceBundle bundle = ResourceBundle.getBundle(name, CharsetPropertiesControl.UTF_8_CONTROL);
      String pattern = bundle.getString(key);
      String message = MessageFormat.format(pattern, arguments);
      return message;
      }

   }
